/**
 * 构建脚本通用工具函数
 *
 * 此文件包含了 Vue.js 构建脚本中广泛使用的工具函数，
 * 包括目标包的扫描和过滤、模糊匹配、子进程执行等功能。
 *
 * @ts-check
 */
import fs from 'node:fs'
import pico from 'picocolors'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'

// 创建 require 函数以支持 CommonJS 模块在 ES 模块中的使用
const require = createRequire(import.meta.url)

/**
 * 扫描并过滤有效的构建目标包
 *
 * 过滤条件：
 * 1. 必须是有效目录
 * 2. 必须包含 package.json 文件
 * 3. 如果是私有包，必须包含 buildOptions 配置
 *
 * 最后添加 'template-explorer' 作为特殊目标
 */
export const targets = fs
  .readdirSync('packages') // 读取 packages 目录
  .filter(f => {
    // 检查是否为有效目录且包含 package.json
    if (
      !fs.statSync(`packages/${f}`).isDirectory() ||
      !fs.existsSync(`packages/${f}/package.json`)
    ) {
      return false
    }

    // 读取包配置
    const pkg = require(`../packages/${f}/package.json`)

    // 过滤没有 buildOptions 的私有包
    if (pkg.private && !pkg.buildOptions) {
      return false
    }

    return true
  })
  .concat('template-explorer') // 添加模板探索器作为特殊目标

/**
 * 模糊匹配目标包
 * 根据部分目标名称在可用目标列表中进行模糊匹配
 *
 * @param {ReadonlyArray<string>} partialTargets - 部分目标名称数组，支持正则表达式匹配
 * @param {boolean | undefined} includeAllMatching - 是否包含所有匹配项，默认仅返回第一个匹配项
 * @returns {Array<string>} 匹配到的目标包数组
 */
export function fuzzyMatchTarget(partialTargets, includeAllMatching) {
  /** @type {Array<string>} 存储匹配结果 */
  const matched = []

  // 遍历每个部分目标
  partialTargets.forEach(partialTarget => {
    // 在所有可用目标中搜索匹配项
    for (const target of targets) {
      if (target.match(partialTarget)) {
        matched.push(target)

        // 如果不需要包含所有匹配项，找到第一个即退出
        if (!includeAllMatching) {
          break
        }
      }
    }
  })

  // 返回匹配结果或报错退出
  if (matched.length) {
    return matched
  } else {
    // 没有找到匹配目标，输出错误信息并退出
    console.log()
    console.error(
      `  ${pico.white(pico.bgRed(' ERROR '))} ${pico.red(
        `Target ${pico.underline(partialTargets.toString())} not found!`,
      )}`,
    )
    console.log()

    process.exit(1)
  }
}

/**
 * 执行外部命令并返回结果
 * 使用 Promise 封装子进程执行，支持跨平台兼容性
 *
 * @param {string} command - 要执行的命令（如 'npm', 'git', 'rollup' 等）
 * @param {ReadonlyArray<string>} args - 命令参数数组
 * @param {object} [options] - spawn 额外选项，将与默认选项合并
 * @returns {Promise<{ok: boolean, code: number, stderr: string, stdout: string}>}
 *   返回包含执行结果的 Promise
 */
export async function exec(command, args, options) {
  return new Promise((resolve, reject) => {
    // 创建子进程
    const _process = spawn(command, args, {
      stdio: [
        'ignore', // 忽略标准输入
        'pipe', // 捕获标准输出
        'pipe', // 捕获错误输出
      ],
      ...options, // 合并用户提供的选项
      shell: process.platform === 'win32', // Windows 下启用 shell 模式
    })

    /**
     * 存储错误输出的 Buffer 数组
     * @type {Buffer[]}
     */
    const stderrChunks = []

    /**
     * 存储标准输出的 Buffer 数组
     * @type {Buffer[]}
     */
    const stdoutChunks = []

    // 监听错误输出数据
    _process.stderr?.on('data', chunk => {
      stderrChunks.push(chunk)
    })

    // 监听标准输出数据
    _process.stdout?.on('data', chunk => {
      stdoutChunks.push(chunk)
    })

    // 监听进程错误（如命令不存在等）
    _process.on('error', error => {
      reject(error)
    })

    // 监听进程退出
    _process.on('exit', code => {
      const ok = code === 0 // 退出码 0 表示成功

      // 将 Buffer 数组转换为字符串并去除首尾空格
      const stderr = Buffer.concat(stderrChunks).toString().trim()
      const stdout = Buffer.concat(stdoutChunks).toString().trim()

      if (ok) {
        // 执行成功，返回结果对象
        const result = { ok, code, stderr, stdout }
        resolve(result)
      } else {
        // 执行失败，抛出包含错误信息的异常
        reject(
          new Error(
            `Failed to execute command: ${command} ${args.join(' ')}: ${stderr}`,
          ),
        )
      }
    })
  })
}
